/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain (a) copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry.ohos.core;

import static io.sentry.ohos.core.NdkIntegration.SENTRY_NDK_CLASS_NAME;

import ohos.aafwk.ability.AbilityPackage;
import ohos.app.Context;
import ohos.bundle.BundleInfo;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;

import io.sentry.ILogger;
import io.sentry.SendCachedEnvelopeFireAndForgetIntegration;
import io.sentry.SendFireAndForgetEnvelopeSender;
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.util.Objects;

/**
 * Ohos Options initializer, it reads configurations from config.json and sets to the
 * SentryOptions. It also adds default values for some fields.
 *
 * @since 2021-06-01
 */
public final class OhosOptionsInitializer {
    /**
     * private ctor
     */
    private OhosOptionsInitializer() {
    }

    /**
     * Init method of the Ohos Options initializer
     *
     * @param options the SentryOptions
     * @param context the Application context
     */
    static void init(final @NotNull SentryOhosOptions options, final @NotNull Context context) {
        Objects.requireNonNull(context, "The application context is required.");
        Objects.requireNonNull(options, "The options object is required.");

        init(options, context, new OhosLogger());
    }

    /**
     * Init method of the Ohos Options initializer
     *
     * @param options the SentryOptions
     * @param context the Application context
     * @param logger the ILogger interface
     */
    public static void init(
        final @NotNull SentryOhosOptions options,
        @NotNull Context context,
        final @NotNull ILogger logger) {
        init(options, context, logger, new BuildInfoProvider());
    }

    /**
     * Init method of the Ohos Options initializer
     *
     * @param options the SentryOptions
     * @param context the Application context
     * @param logger the ILogger interface
     * @param buildInfoProvider the IBuildInfoProvider interface
     */
    static void init(
        final @NotNull SentryOhosOptions options,
        @NotNull Context context,
        final @NotNull ILogger logger,
        final @NotNull IBuildInfoProvider buildInfoProvider) {
        init(options, context, logger, buildInfoProvider, new LoadClass());
    }

    /**
     * Init method of the Ohos Options initializer
     *
     * @param options the SentryOptions
     * @param context the Application context
     * @param logger the ILogger interface
     * @param buildInfoProvider the IBuildInfoProvider interface
     * @param loadClass the ILoadClass interface
     */
    static void init(
        final @NotNull SentryOhosOptions options,
        @NotNull Context context,
        final @NotNull ILogger logger,
        final @NotNull IBuildInfoProvider buildInfoProvider,
        final @NotNull ILoadClass loadClass) {
        Objects.requireNonNull(context, "The context is required.");

        // it returns null if ContextImpl, so let's check for nullability
        if (context.getApplicationContext() != null) {
            context = context.getApplicationContext();
        }

        Objects.requireNonNull(options, "The options object is required.");
        Objects.requireNonNull(logger, "The ILogger object is required.");

        // Firstly set the logger, if `debug=true` configured, logging can start asap.
        options.setLogger(logger);

        ManifestMetadataReader.applyMetadata(context, options);
        initializeCacheDirs(context, options);

        installDefaultIntegrations(context, options, buildInfoProvider, loadClass);

        readDefaultOptionValues(options, context);

        options.addEventProcessor(new DefaultOhosEventProcessor(context, logger, buildInfoProvider));

        options.setTransportGate(new OhosTransportGate(context, options.getLogger()));
    }

    private static void installDefaultIntegrations(
        final @NotNull Context context,
        final @NotNull SentryOptions options,
        final @NotNull IBuildInfoProvider buildInfoProvider,
        final @NotNull ILoadClass loadClass) {
        options.addIntegration(
            new SendCachedEnvelopeFireAndForgetIntegration(
                new SendFireAndForgetEnvelopeSender(() -> options.getCacheDirPath())));

        // Integrations are registered in the same order. NDK before adding Watch outbox,
        // because sentry-native move files around and we don't want to watch that.
        final Class<?> sentryNdkClass = loadNdkIfAvailable(options, buildInfoProvider, loadClass);
        options.addIntegration(new NdkIntegration(sentryNdkClass));

        // before creating (a) pure java impl.
        options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver());

        // Send cached envelopes from outbox path
        // this should be executed after NdkIntegration because sentry-native move files on init.
        // and we'd like to send them right away
        options.addIntegration(
            new SendCachedEnvelopeFireAndForgetIntegration(
                new SendFireAndForgetOutboxSender(() -> options.getOutboxPath())));

        options.addIntegration(new AnrIntegration(context));
        options.addIntegration(new AppLifecycleIntegration());

        // registerAbilityLifecycleCallbacks is only available if Context is an AppContext
        if (context instanceof AbilityPackage) {
            options.addIntegration(
                new AbilityLifecycleIntegration((AbilityPackage) context, buildInfoProvider));
        } else {
            options
                .getLogger()
                .log(
                    SentryLevel.WARNING,
                    "AbilityBreadcrumbsIntegration needs an Application class to be installed.");
        }
        options.addIntegration(new AppComponentsBreadcrumbsIntegration(context));
        options.addIntegration(new SystemEventsBreadcrumbsIntegration(context));
        options.addIntegration(new TempSensorBreadcrumbsIntegration(context));
        options.addIntegration(new PhoneStateBreadcrumbsIntegration(context));
    }

    /**
     * Reads and sets default option values that are Ohos specific like release and inApp
     *
     * @param options the SentryOptions
     * @param context the Ohos context methods
     */
    private static void readDefaultOptionValues(
        final @NotNull SentryOhosOptions options, final @NotNull Context context) {
        final BundleInfo bundleInfo = ContextUtils.getPackageInfo(context, options.getLogger());
        if (bundleInfo != null) {
            // Sets App's release if not set by Manifest
            if (options.getRelease() == null) {
                options.setRelease(
                    getSentryReleaseVersion(bundleInfo, ContextUtils.getVersionCode(bundleInfo)));
            }

            // Sets the App's package name as InApp
            final String packageName = bundleInfo.getName();
            if (packageName != null && !packageName.startsWith("ohos.")) {
                options.addInAppInclude(packageName);
            }
        }

        if (options.getDistinctId() == null) {
            try {
                options.setDistinctId(Installation.id(context));
            } catch (IOException e) {
                options.getLogger().log(SentryLevel.ERROR, "Could not generate distinct Id.", e);
            }
        }
    }

    /**
     * Returns the sentry release version (eg io.sentry.sample@1.0.0+10000) -
     * packageName versionName buildVersion
     *
     * @param bundleInfo the PackageInfo
     * @param versionCode the versionCode
     * @return the sentry release version as (a) String
     */
    private static @NotNull String getSentryReleaseVersion(
        final @NotNull BundleInfo bundleInfo, final @NotNull String versionCode) {
        return bundleInfo.getName() + "@" + bundleInfo.getVersionCode() + "+" + versionCode;
    }

    /**
     * Sets the cache dirs like sentry, outbox and sessions
     *
     * @param context the Application context
     * @param options the SentryOptions
     */
    private static void initializeCacheDirs(
        final @NotNull Context context, final @NotNull SentryOptions options) {
        final File cacheDir = new File(context.getCacheDir(), "sentry");
        try {
            options.setCacheDirPath(cacheDir.getCanonicalPath());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInfoProvider) {
        return true;
    }

    private static @Nullable Class<?> loadNdkIfAvailable(
        final @NotNull SentryOptions options,
        final @NotNull IBuildInfoProvider buildInfoProvider,
        final @NotNull ILoadClass loadClass) {
        if (isNdkAvailable(buildInfoProvider)) {
            try {
                return loadClass.loadClass(SENTRY_NDK_CLASS_NAME);
            } catch (ClassNotFoundException e) {
                options.getLogger().log(SentryLevel.ERROR, "Failed to load SentryNdk.", e);
            } catch (UnsatisfiedLinkError e) {
                options
                    .getLogger()
                    .log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) SentryNdk.", e);
            } catch (Throwable e) {
                options.getLogger().log(SentryLevel.ERROR, "Failed to initialize SentryNdk.", e);
            }
        }
        return null;
    }
}
